iT邦幫忙

2026 iThome 鐵人賽

DAY 3
0
Kubernetes

初見 Kubernetes - 純網站後端開發踏入 K8s 世界的經驗分享系列 第 3

Day 3 - Dockerfile、Image 與 Docker Compose

  • 分享至 

  • xImage
  •  

前一天我們提到了 VM 與容器的差異,那麼今天我們就快速了解一下,要怎麼撰寫一份 Dockerfile、建立一個 image,以及啟動一個容器,也會提一下 Docker Compose 如何一次啟動多個容器,因為實際上在部署 K8s 時,通常都是將既有架構下的多個容器遷移進 K8s 內,了解 Docker Compose 其實也有助於釐清現行架構有哪些服務是有關聯的。

這邊我會使用 ASP.NET Core API 作為舉例,透過 SDK 與 runtime 的分離,說明 multi-stage build 的概念。

Dockerfile

Dockerfile 是文字檔,依序描述 Docker 如何取得基底環境、複製檔案、執行建置指令,最後決定 container 啟動時要使用甚麼環境變數以及跑什麼程式。

有幾個常見的名詞可以先釐清:

  • Dockerfile:建置 image 的主要依據,可以當它是一個藍圖。
  • Image:依據 Dockerfile 將應用程式、必要 runtime 與檔案系統 layer 進行打包而產出的結果。
  • Container:容器,用 image 建立讓它是一個有服務的成品。
  • Docker Compose:當有多個 container 需要一起啟動、網路切分、傳入設定與掛載資料時使用的工具。

Dockerfile 處理的是「一個服務如何被封裝」,而 Compose 處理「多個服務如何在本機一起運作」。

為什麼需要 SDK 與 Runtime 兩種 image

熟悉程式開發的人,我想都知道程式從原始碼到能運作執行,主要會經過兩個階段,以 ASP.NET Core API 為例:

  1. 建置:需要 .NET SDK,對程式檔執行 buildpublish
  2. 執行階段:需要 ASP.NET Core runtime 與 .NET SDK publish 產出的檔案。

從上面這兩點可以得知,實際上要讓程式運行,最終只需要 Runtime 來運行程式,SDK 只有建置程式碼時才需要,
所以在製作 Image 時,我們就可以不需要連 SDK 都一起打包。

如果把 SDK、原始碼、NuGet 快取與建置工具全部放進最後的 production image,容器雖然還是能跑,但 image 會更大,也帶進容器運作時不需要的內容,有時候更會因此增加 Image 的資安風險(例如 SDK 的 base image 有新的 CVE 漏洞等...),所以在規劃 Dockerfile 時,最好還是往最輕量化的方向去設計還是比較好的。

multi-stage build

Dockerfile 可以有多個 FROM,每個 FROM 都開始一個新的 stage,最後的 stage 只用 COPY --from=<stage> 取得所需要的資源。
前一個 stage 的 SDK、原始碼或是中間使用到的任何檔案、資源都不會出現在最終打包的 image 內,這就是 multi-stage build 的意義所在。

以下是一個製作 ASP.NET Core API 的 Dockerfile 範例:

# syntax=docker/dockerfile:1
ARG DOTNET_VERSION=10.0

FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} AS build
WORKDIR /src

# 先複製專案檔並還原相依套件。
COPY ["src/Workspace.Api/Workspace.Api.csproj", "src/Workspace.Api/"]
RUN dotnet restore "src/Workspace.Api/Workspace.Api.csproj"

# 再複製其餘原始碼並產生可部署的檔案。
# 第一個 . 代表 Dockerfile 的上下文目錄,也就是整個專案根目錄。
# 第二個 . 代表要複製到的目標目錄,也就是 WORKDIR 所指向的目錄。
COPY . .
WORKDIR /src/src/Workspace.Api
RUN dotnet publish -c Release -o /out --no-restore

# 打包,最終 image 只有 ASP.NET Core runtime 與 publish 檔案結果。
FROM mcr.microsoft.com/dotnet/aspnet:${DOTNET_VERSION} AS final

# 設定最終 image 的工作目錄。
WORKDIR /app

# 設定環境變數與工作目錄。
ENV ASPNETCORE_HTTP_PORTS=8080

# 說明這個 container 對外提供的 port
EXPOSE 8080

# 複製建置結果到最終 image。
COPY --from=build /out .

# 以非 root 使用者執行,增加容器安全性。
USER app

# 指定 container 啟動後要執行的命令。
ENTRYPOINT ["dotnet", "Workspace.Api.dll"]

這份 Dockerfile 的流程如下:

先複製 .csproj 並執行 restore,再複製全部原始碼;當只有程式碼改變、相依套件沒有改變時,Docker 可以重用先前的 restore 結果,避免每次都重新下載 NuGet 套件。

https://ithelp.ithome.com.tw/upload/images/20260918/20124323OLyl57LMRO.png

multi-stage build 說明的是單一服務如何從 Dockerfile 產生 image;Docker Compose 則是另一個責任層,用來描述多個已建置完成的 container 如何一起啟動與協作。兩者的邊界如下圖:

https://ithelp.ithome.com.tw/upload/images/20260918/20124323RrZAHP34Bg.png

不同類型的 base image

時常會聽到容器服務基本上都跑在 Alpine Linux 上,其實也不一定。以 .NET 來說,.NET 10 官方 image 預設是 Ubuntu,而也有提供 Alpine、Ubuntu chiseled 等選項。

  • Alpine 的體積較小,適合此程式、容器該使用哪些套件並且由開發者自行管理的情況
  • Ubuntu 類型的 image 通常有較完整的相容性與較多預設就安裝好的套件
  • chiseled 或 distroless image 會再移除 shell、package manager 等元件,更輕量的情況下,讓套件成為漏洞的機率也縮小了,但後續如果要自行安裝套件或在容器運作時 debug,就需要額外的處理

Docker Compose:讓多個 Container 一起運作

前面寫的 Dockerfile,處理的是單一 ASP.NET Core API 如何被建置成 image,但實際的服務環境通常不會只有一個 API,可能還會有其他 API、資料庫、快取或前端服務。

這時候就可以使用 Docker Compose,藉由讀取撰寫好的 docker-compose.yml,依照裡面的 services 定義建立多個 container,並處理它們之間的網路、環境變數與 volume 掛載。

docker run 啟動單一 Container

在需要一次啟動多個服務之前,可以先用 docker run 驗證剛剛建立的 image 能不能正常運作。

以下指令會在背景啟動 workspace-api,並把本機的 8080 port 對應到 container 內 ASP.NET Core API 監聽的 8080 port:

docker run --name workspace-api --rm -d -p 8080:8080 workshop/workspace-api:dev
  • --name workspace-api:替這個 container 指定容易辨識的名稱。
  • --rm:container 停止後自動刪除,適合本機測試;若要保留停止後的 container 來 debug,則可以先不使用這個參數。
  • -d:在背景執行,若不加 -d,應用程式的輸出會直接顯示在終端機。
  • -p 8080:8080:前面的 8080 是本機電腦的 port,後面的 8080 是 container 內程式監聽的 port。
  • workshop/workspace-api:dev:要啟動的 image 名稱與 tag。

啟動後可以使用 docker ps 確認 container 是否還在執行,或以 docker logs workspace-api 查看應用程式輸出。

使用 Docker Compose 啟動多個服務

以下是一個簡化後的範例:

  • api-gateway 提供 API 的統一入口,並將請求轉送到 directory-apiworkspace-api
  • workspace-api 也會呼叫 directory-api
  • 兩個 API 都會使用 PostgreSQL。
services:
  api-gateway:
    image: workshop/api-gateway:dev
    ports:
      - "8080:8080"
    environment:
      DirectoryApi__BaseUrl: http://directory-api:8080
      WorkspaceApi__BaseUrl: http://workspace-api:8080
    depends_on:
      directory-api:
        condition: service_started
      workspace-api:
        condition: service_started

  directory-api:
    image: workshop/directory-api:dev
    environment:
      ConnectionStrings__AppDb: Host=postgres;Port=5432;Database=workspace;Username=app;Password=dev-password
    depends_on:
      postgres:
        condition: service_healthy

  workspace-api:
    image: workshop/workspace-api:dev
    environment:
      DirectoryApi__BaseUrl: http://directory-api:8080
      ConnectionStrings__AppDb: Host=postgres;Port=5432;Database=workspace;Username=app;Password=dev-password
    depends_on:
      directory-api:
        condition: service_started
      postgres:
        condition: service_healthy

  postgres:
    image: postgres:17
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: dev-password
      POSTGRES_DB: workspace
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d workspace"]
      interval: 5s
      timeout: 3s
      retries: 10

volumes:
  postgres-data:

這份 Docker Compose 有以下幾個重點:

  • services 下的名稱同時是 Compose 管理的服務名稱。workspace-api 可以直接用 http://directory-api:8080 呼叫另一個服務,不需要先查 container IP。
  • api-gateway 是 API 的統一入口。前端或外部工具只需要知道 Gateway 的位址;Gateway 再依 path 或其他路由規則轉送到內部 API。
  • directory-apiworkspace-api 沒有設定 ports,因此不會直接對本機電腦開放。
  • ports: "8080:8080" 的前面是本機電腦的 port,後面才是 container 內程式實際監聽的 port。只有需要從本機瀏覽器或像是 postman 的工具要直接呼叫該容器的服務,才需要設定這一段。
  • environment 是在 container 啟動時注入設定的方式,以這一個範例來說,PostgreSQL 的連線字串與 API 的 BaseUrl 都是透過 environment 環境變數注入的。
  • volumes 讓 PostgreSQL 的資料不會因為 container 被刪除就一起消失。
  • depends_on 可以協助控制啟動順序,搭配 service_healthy 時可等待資料庫 healthcheck 成功才啟動後續服務。

完成設定後,可以使用以下指令在背景啟動所有的容器:

docker compose up -d

此時 Docker Compose 會建立一個預設 network,讓這份 Docker Compose 中的容器能透過 service 名稱互相連線。

後續把服務搬到 Kubernetes 時,這個「以 Service 名稱而不是透過 IP 呼叫服務」的概念會繼續使用,但實作會改由 Kubernetes Service 與 CoreDNS 接手。

本次系列文章的微服務架構

以下是本次主題會使用的微服務架構,後續會將這些運作在 Docker Compose 的服務,逐一部署到 Kubernetes:

https://ithelp.ithome.com.tw/upload/images/20260918/20124323b6oPar2NGp.png

  • portal-web 負責入口與前端整合,而兩個前端的 API 請求則統一先進入 api-gateway,Gateway 再依路徑將請求轉送到內部 API,因此 directory-apiworkspace-api 不會直接對外。

  • workspace-api 若需要共用目錄資料,仍可以在內部以 east-west 流量呼叫 directory-api


上一篇
Day 2 - 容器與 VM 的差異:OS、Kernel 與執行環境
下一篇
Day 4 - 本次微服務範例:Portal、Gateway、API 與 Stateless
系列文
初見 Kubernetes - 純網站後端開發踏入 K8s 世界的經驗分享9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言